diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-20 18:38:24 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-20 18:38:24 +0000 |
commit | 983a0bba5d2a042c4a3bbb22432ec192c7501d82 (patch) | |
tree | b153cd387c14ba23bd5a07514c7c01fddf6a78a0 /lib | |
parent | a2bddee2cdb38673df0e004d5b32d9f77797de64 (diff) | |
download | gitlab-ce-983a0bba5d2a042c4a3bbb22432ec192c7501d82.tar.gz |
Add latest changes from gitlab-org/gitlab@12-10-stable-ee
Diffstat (limited to 'lib')
64 files changed, 1050 insertions, 311 deletions
diff --git a/lib/api/deploy_tokens.rb b/lib/api/deploy_tokens.rb index 5de36c14d7b..f3a08ae970a 100644 --- a/lib/api/deploy_tokens.rb +++ b/lib/api/deploy_tokens.rb @@ -8,7 +8,7 @@ module API def scope_params scopes = params.delete(:scopes) - result_hash = {} + result_hash = Hashie::Mash.new result_hash[:read_registry] = scopes.include?('read_registry') result_hash[:write_registry] = scopes.include?('write_registry') result_hash[:read_repository] = scopes.include?('read_repository') diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index 8ff275a3a1b..0dd1850e526 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -74,6 +74,11 @@ module API optional :height, type: Integer, desc: 'Height of the image' optional :x, type: Integer, desc: 'X coordinate in the image' optional :y, type: Integer, desc: 'Y coordinate in the image' + + optional :line_range, type: Hash, desc: 'Multi-line start and end' do + requires :start_line_code, type: String, desc: 'Start line code for multi-line note' + requires :end_line_code, type: String, desc: 'End line code for multi-line note' + end end end post ":id/#{noteables_path}/:noteable_id/discussions" do diff --git a/lib/api/entities/container_expiration_policy.rb b/lib/api/entities/container_expiration_policy.rb index 853bbb9b76b..b2240704b99 100644 --- a/lib/api/entities/container_expiration_policy.rb +++ b/lib/api/entities/container_expiration_policy.rb @@ -8,6 +8,7 @@ module API expose :keep_n expose :older_than expose :name_regex + expose :name_regex_keep expose :next_run_at end end diff --git a/lib/api/entities/merge_request.rb b/lib/api/entities/merge_request.rb index 9ff8e20ced1..7fc76a4071e 100644 --- a/lib/api/entities/merge_request.rb +++ b/lib/api/entities/merge_request.rb @@ -39,6 +39,16 @@ module API expose :diverged_commits_count, as: :diverged_commits_count, if: -> (_, options) { options[:include_diverged_commits_count] } + # We put this into an option because list of TODOs API will attach their + # targets with Entities::MergeRequest instead of + # Entities::MergeRequestBasic, but this attribute cannot be eagerly + # loaded in batch for now. The list of merge requests API will + # use Entities::MergeRequestBasic which does not support this, and + # we always enable this for the single merge request API. This way + # we avoid N+1 queries in the TODOs API and can still enable it for + # the single merge request API. + expose :first_contribution?, as: :first_contribution, if: -> (_, options) { options[:include_first_contribution] } + def build_available?(options) options[:project]&.feature_available?(:builds, options[:current_user]) end diff --git a/lib/api/entities/merge_request_basic.rb b/lib/api/entities/merge_request_basic.rb index 8cec2c1a97e..4610220e4f6 100644 --- a/lib/api/entities/merge_request_basic.rb +++ b/lib/api/entities/merge_request_basic.rb @@ -52,7 +52,7 @@ module API # information. expose :merge_status do |merge_request| merge_request.check_mergeability(async: true) - merge_request.merge_status + merge_request.public_merge_status end expose :diff_head_sha, as: :sha expose :merge_commit_sha diff --git a/lib/api/entities/project_import_failed_relation.rb b/lib/api/entities/project_import_failed_relation.rb new file mode 100644 index 00000000000..16b26ad0efa --- /dev/null +++ b/lib/api/entities/project_import_failed_relation.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectImportFailedRelation < Grape::Entity + expose :id, :created_at, :exception_class, :exception_message, :source + + expose :relation_key, as: :relation_name + end + end +end diff --git a/lib/api/entities/project_import_status.rb b/lib/api/entities/project_import_status.rb index de7b4b998be..5ef5600259f 100644 --- a/lib/api/entities/project_import_status.rb +++ b/lib/api/entities/project_import_status.rb @@ -8,6 +8,10 @@ module API project.import_state&.correlation_id end + expose :failed_relations, using: Entities::ProjectImportFailedRelation do |project, _options| + project.import_state.relation_hard_failures(limit: 100) + end + # TODO: Use `expose_nil` once we upgrade the grape-entity gem expose :import_error, if: lambda { |project, _ops| project.import_state&.last_error } do |project| project.import_state.last_error diff --git a/lib/api/entities/user.rb b/lib/api/entities/user.rb index 4a1f570c3f0..adf954ab02d 100644 --- a/lib/api/entities/user.rb +++ b/lib/api/entities/user.rb @@ -3,8 +3,12 @@ module API module Entities class User < UserBasic + include UsersHelper expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) } expose :bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title + expose :work_information do |user| + work_information(user) + end end end end diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index f7aabc8ce4f..31272c537a3 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -3,7 +3,7 @@ module API module Helpers module InternalHelpers - attr_reader :redirected_path, :container + attr_reader :redirected_path delegate :wiki?, to: :repo_type @@ -11,15 +11,22 @@ module API @actor ||= Support::GitAccessActor.from_params(params) end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def repo_type - set_project unless defined?(@repo_type) # rubocop:disable Gitlab/ModuleWithInstanceVariables - @repo_type # rubocop:disable Gitlab/ModuleWithInstanceVariables + parse_repo_path unless defined?(@repo_type) + @repo_type end def project - set_project unless defined?(@project) # rubocop:disable Gitlab/ModuleWithInstanceVariables - @project # rubocop:disable Gitlab/ModuleWithInstanceVariables + parse_repo_path unless defined?(@project) + @project + end + + def container + parse_repo_path unless defined?(@container) + @container end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def access_checker_for(actor, protocol) access_checker_klass.new(actor.key_or_user, container, protocol, @@ -79,7 +86,7 @@ module API end # rubocop:disable Gitlab/ModuleWithInstanceVariables - def set_project + def parse_repo_path @container, @project, @repo_type, @redirected_path = if params[:gl_repository] Gitlab::GlRepository.parse(params[:gl_repository]) diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 8ad682fc961..14c83114f32 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -85,6 +85,7 @@ module API optional :keep_n, type: String, desc: 'Container expiration policy number of images to keep' optional :older_than, type: String, desc: 'Container expiration policy remove images older than value' optional :name_regex, type: String, desc: 'Container expiration policy regex for image removal' + optional :name_regex_keep, type: String, desc: 'Container expiration policy regex for image retention' optional :enabled, type: Boolean, desc: 'Flag indication if container expiration policy is enabled' end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 4d9f035e2cd..d45786cdd3d 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -267,6 +267,7 @@ module API current_user: current_user, project: user_project, render_html: params[:render_html], + include_first_contribution: true, include_diverged_commits_count: params[:include_diverged_commits_count], include_rebase_in_progress: params[:include_rebase_in_progress] end diff --git a/lib/api/project_statistics.rb b/lib/api/project_statistics.rb index 2f73785f72d..14ee0f75513 100644 --- a/lib/api/project_statistics.rb +++ b/lib/api/project_statistics.rb @@ -4,7 +4,6 @@ module API class ProjectStatistics < Grape::API before do authenticate! - not_found! unless user_project.daily_statistics_enabled? authorize! :daily_statistics, user_project end diff --git a/lib/api/services.rb b/lib/api/services.rb index a3b5d2cc4b7..5fd5c6bd9b0 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -130,13 +130,11 @@ module API TRIGGER_SERVICES.each do |service_slug, settings| helpers do - # rubocop: disable CodeReuse/ActiveRecord def slash_command_service(project, service_slug, params) - project.services.active.where(template: false).find do |service| + project.services.active.find do |service| service.try(:token) == params[:token] && service.to_param == service_slug.underscore end end - # rubocop: enable CodeReuse/ActiveRecord end params do diff --git a/lib/api/terraform/state.rb b/lib/api/terraform/state.rb index 7e55dfedfeb..052c75188ab 100644 --- a/lib/api/terraform/state.rb +++ b/lib/api/terraform/state.rb @@ -1,41 +1,117 @@ # frozen_string_literal: true +require_dependency 'api/validations/validators/limit' + module API module Terraform class State < Grape::API - before { authenticate! } - before { authorize! :admin_terraform_state, user_project } + include ::Gitlab::Utils::StrongMemoize + + default_format :json + + before do + authenticate! + authorize! :admin_terraform_state, user_project + end params do requires :id, type: String, desc: 'The ID of a project' end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - params do - requires :name, type: String, desc: 'The name of a terraform state' - end namespace ':id/terraform/state/:name' do + params do + requires :name, type: String, desc: 'The name of a Terraform state' + optional :ID, type: String, limit: 255, desc: 'Terraform state lock ID' + end + + helpers do + def remote_state_handler + ::Terraform::RemoteStateHandler.new(user_project, current_user, name: params[:name], lock_id: params[:ID]) + end + end + desc 'Get a terraform state by its name' route_setting :authentication, basic_auth_personal_access_token: true get do - status 501 - content_type 'text/plain' - body 'not implemented' + remote_state_handler.find_with_lock do |state| + no_content! unless state.file.exists? + + env['api.format'] = :binary # this bypasses json serialization + body state.file.read + status :ok + end end desc 'Add a new terraform state or update an existing one' route_setting :authentication, basic_auth_personal_access_token: true post do - status 501 - content_type 'text/plain' - body 'not implemented' + data = request.body.string + no_content! if data.empty? + + remote_state_handler.handle_with_lock do |state| + state.file = CarrierWaveStringFile.new(data) + state.save! + status :ok + end end - desc 'Delete a terraform state of certain name' + desc 'Delete a terraform state of a certain name' route_setting :authentication, basic_auth_personal_access_token: true delete do - status 501 - content_type 'text/plain' - body 'not implemented' + remote_state_handler.handle_with_lock do |state| + state.destroy! + status :ok + end + end + + desc 'Lock a terraform state of a certain name' + route_setting :authentication, basic_auth_personal_access_token: true + params do + requires :ID, type: String, limit: 255, desc: 'Terraform state lock ID' + requires :Operation, type: String, desc: 'Terraform operation' + requires :Info, type: String, desc: 'Terraform info' + requires :Who, type: String, desc: 'Terraform state lock owner' + requires :Version, type: String, desc: 'Terraform version' + requires :Created, type: String, desc: 'Terraform state lock timestamp' + requires :Path, type: String, desc: 'Terraform path' + end + post '/lock' do + status_code = :ok + lock_info = { + 'Operation' => params[:Operation], + 'Info' => params[:Info], + 'Version' => params[:Version], + 'Path' => params[:Path] + } + + begin + remote_state_handler.lock! + rescue ::Terraform::RemoteStateHandler::StateLockedError + status_code = :conflict + end + + remote_state_handler.find_with_lock do |state| + lock_info['ID'] = state.lock_xid + lock_info['Who'] = state.locked_by_user.username + lock_info['Created'] = state.locked_at + + env['api.format'] = :binary # this bypasses json serialization + body lock_info.to_json + status status_code + end + end + + desc 'Unlock a terraform state of a certain name' + route_setting :authentication, basic_auth_personal_access_token: true + params do + optional :ID, type: String, limit: 255, desc: 'Terraform state lock ID' + end + delete '/lock' do + remote_state_handler.unlock! + status :ok + rescue ::Terraform::RemoteStateHandler::StateLockedError + status :conflict end end end diff --git a/lib/api/validations/validators/limit.rb b/lib/api/validations/validators/limit.rb new file mode 100644 index 00000000000..3bb4cee1d75 --- /dev/null +++ b/lib/api/validations/validators/limit.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module API + module Validations + module Validators + class Limit < Grape::Validations::Base + def validate_param!(attr_name, params) + value = params[attr_name] + + return if value.size <= @option + + raise Grape::Exceptions::Validation, + params: [@scope.full_name(attr_name)], + message: "#{@scope.full_name(attr_name)} must be less than #{@option} characters" + end + end + end + end +end diff --git a/lib/banzai/pipeline.rb b/lib/banzai/pipeline.rb index 497d3f27542..8fdbc044861 100644 --- a/lib/banzai/pipeline.rb +++ b/lib/banzai/pipeline.rb @@ -2,9 +2,33 @@ module Banzai module Pipeline + # Resolve a pipeline by name + # + # name - nil, Class or Symbol. The name to be resolved. + # + # Examples: + # Pipeline[nil] # => Banzai::Pipeline::FullPipeline + # Pipeline[:label] # => Banzai::Pipeline::LabelPipeline + # Pipeline[StatusPage::PostProcessPipeline] # => StatusPage::PostProcessPipeline + # + # Pipeline['label'] # => raises ArgumentError - unsupport type + # Pipeline[Project] # => raises ArgumentError - not a subclass of BasePipeline + # + # Returns a pipeline class which is a subclass of Banzai::Pipeline::BasePipeline. def self.[](name) - name ||= :full - const_get("#{name.to_s.camelize}Pipeline", false) + name ||= FullPipeline + + pipeline = case name + when Class + name + when Symbol + const_get("#{name.to_s.camelize}Pipeline", false) + end + + return pipeline if pipeline && pipeline < BasePipeline + + raise ArgumentError, + "unsupported pipeline name #{name.inspect} (#{name.class})" end end end diff --git a/lib/csv_builder.rb b/lib/csv_builder.rb new file mode 100644 index 00000000000..7df4e3bf85d --- /dev/null +++ b/lib/csv_builder.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +# Generates CSV when given a collection and a mapping. +# +# Example: +# +# columns = { +# 'Title' => 'title', +# 'Comment' => 'comment', +# 'Author' => -> (post) { post.author.full_name } +# 'Created At (UTC)' => -> (post) { post.created_at&.strftime('%Y-%m-%d %H:%M:%S') } +# } +# +# CsvBuilder.new(@posts, columns).render +# +class CsvBuilder + attr_reader :rows_written + + # + # * +collection+ - The data collection to be used + # * +header_to_hash_value+ - A hash of 'Column Heading' => 'value_method'. + # + # The value method will be called once for each object in the collection, to + # determine the value for that row. It can either be the name of a method on + # the object, or a lamda to call passing in the object. + def initialize(collection, header_to_value_hash) + @header_to_value_hash = header_to_value_hash + @collection = collection + @truncated = false + @rows_written = 0 + end + + # Renders the csv to a string + def render(truncate_after_bytes = nil) + Tempfile.open(['csv']) do |tempfile| + csv = CSV.new(tempfile) + + write_csv csv, until_condition: -> do + truncate_after_bytes && tempfile.size > truncate_after_bytes + end + + if block_given? + yield tempfile + else + tempfile.rewind + tempfile.read + end + end + end + + def truncated? + @truncated + end + + def rows_expected + if truncated? || rows_written == 0 + @collection.count + else + rows_written + end + end + + def status + { + truncated: truncated?, + rows_written: rows_written, + rows_expected: rows_expected + } + end + + private + + def headers + @headers ||= @header_to_value_hash.keys + end + + def attributes + @attributes ||= @header_to_value_hash.values + end + + def row(object) + attributes.map do |attribute| + if attribute.respond_to?(:call) + excel_sanitize(attribute.call(object)) + else + excel_sanitize(object.public_send(attribute)) # rubocop:disable GitlabSecurity/PublicSend + end + end + end + + def write_csv(csv, until_condition:) + csv << headers + + @collection.find_each do |object| + csv << row(object) + + @rows_written += 1 + + if until_condition.call + @truncated = true + break + end + end + end + + def excel_sanitize(line) + return if line.nil? + + line = ["'", line].join if line =~ /^[=\+\-@;]/ + line + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events.rb b/lib/gitlab/analytics/cycle_analytics/stage_events.rb index f6e22044142..5146f92f521 100644 --- a/lib/gitlab/analytics/cycle_analytics/stage_events.rb +++ b/lib/gitlab/analytics/cycle_analytics/stage_events.rb @@ -24,6 +24,13 @@ module Gitlab EVENTS = ENUM_MAPPING.keys.freeze + INTERNAL_EVENTS = [ + StageEvents::CodeStageStart, + StageEvents::IssueStageEnd, + StageEvents::PlanStageStart, + StageEvents::ProductionStageEnd + ].freeze + # Defines which start_event and end_event pairs are allowed PAIRING_RULES = { StageEvents::PlanStageStart => [ @@ -67,6 +74,11 @@ module Gitlab def self.enum_mapping ENUM_MAPPING end + + # Events that are specific to the 7 default stages + def self.internal_events + INTERNAL_EVENTS + end end end end diff --git a/lib/gitlab/application_context.rb b/lib/gitlab/application_context.rb index 60a50e97998..a3feda9bb59 100644 --- a/lib/gitlab/application_context.rb +++ b/lib/gitlab/application_context.rb @@ -25,6 +25,10 @@ module Gitlab Labkit::Context.push(application_context.to_lazy_hash) end + def self.current_context_include?(attribute_name) + Labkit::Context.current.to_h.include?(Labkit::Context.log_key(attribute_name)) + end + def initialize(**args) unknown_attributes = args.keys - APPLICATION_ATTRIBUTES.map(&:name) raise ArgumentError, "#{unknown_attributes} are not known keys" if unknown_attributes.any? diff --git a/lib/gitlab/ci/jwt.rb b/lib/gitlab/ci/jwt.rb new file mode 100644 index 00000000000..491facd0a43 --- /dev/null +++ b/lib/gitlab/ci/jwt.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Jwt + NOT_BEFORE_TIME = 5 + DEFAULT_EXPIRE_TIME = 60 * 5 + + def self.for_build(build) + self.new(build, ttl: build.metadata_timeout).encoded + end + + def initialize(build, ttl: nil) + @build = build + @ttl = ttl + end + + def payload + custom_claims.merge(reserved_claims) + end + + def encoded + headers = { kid: kid, typ: 'JWT' } + + JWT.encode(payload, key, 'RS256', headers) + end + + private + + attr_reader :build, :ttl, :key_data + + def reserved_claims + now = Time.now.to_i + + { + jti: SecureRandom.uuid, + iss: Settings.gitlab.host, + iat: now, + nbf: now - NOT_BEFORE_TIME, + exp: now + (ttl || DEFAULT_EXPIRE_TIME), + sub: "job_#{build.id}" + } + end + + def custom_claims + { + namespace_id: namespace.id.to_s, + namespace_path: namespace.full_path, + project_id: project.id.to_s, + project_path: project.full_path, + user_id: user&.id.to_s, + user_login: user&.username, + user_email: user&.email, + pipeline_id: build.pipeline.id.to_s, + job_id: build.id.to_s, + ref: source_ref, + ref_type: ref_type, + ref_protected: build.protected.to_s + } + end + + def key + @key ||= OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key) + end + + def public_key + key.public_key + end + + def kid + public_key.to_jwk[:kid] + end + + def project + build.project + end + + def namespace + project.namespace + end + + def user + build.user + end + + def source_ref + build.pipeline.source_ref + end + + def ref_type + ::Ci::BuildRunnerPresenter.new(build).ref_type + end + end + end +end diff --git a/lib/gitlab/ci/status/bridge/factory.rb b/lib/gitlab/ci/status/bridge/factory.rb index 910de865483..5d397dba0de 100644 --- a/lib/gitlab/ci/status/bridge/factory.rb +++ b/lib/gitlab/ci/status/bridge/factory.rb @@ -5,6 +5,10 @@ module Gitlab module Status module Bridge class Factory < Status::Factory + def self.extended_statuses + [Status::Bridge::Failed] + end + def self.common_helpers Status::Bridge::Common end diff --git a/lib/gitlab/ci/status/bridge/failed.rb b/lib/gitlab/ci/status/bridge/failed.rb new file mode 100644 index 00000000000..de7446c238c --- /dev/null +++ b/lib/gitlab/ci/status/bridge/failed.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Status + module Bridge + class Failed < Status::Build::Failed + end + end + end + end +end diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml index 6b72db951ed..3949b87bbda 100644 --- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml @@ -1,6 +1,6 @@ build: stage: build - image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image:v0.2.1" + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image:v0.2.2" variables: DOCKER_TLS_CERTDIR: "" services: diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml index c6c8256b4bb..9bf0d31409a 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml @@ -1,5 +1,5 @@ .auto-deploy: - image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.12.1" + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.13.0" review: extends: .auto-deploy diff --git a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml index 713b11c4d8f..54a29b04d39 100644 --- a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml @@ -1,6 +1,6 @@ apply: stage: deploy - image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.13.1" + image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.15.0" environment: name: production variables: @@ -17,6 +17,8 @@ apply: ELASTIC_STACK_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/elastic-stack/values.yaml VAULT_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/vault/values.yaml CROSSPLANE_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/crossplane/values.yaml + FLUENTD_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/fluentd/values.yaml + KNATIVE_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/knative/values.yaml script: - gitlab-managed-apps /usr/local/share/gitlab-managed-apps/helmfile.yaml only: diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml index 717e91b3ae5..0ecf37b37a3 100644 --- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml @@ -57,6 +57,8 @@ dependency_scanning: PIP_EXTRA_INDEX_URL \ PIP_REQUIREMENTS_FILE \ MAVEN_CLI_OPTS \ + GRADLE_CLI_OPTS \ + SBT_CLI_OPTS \ BUNDLER_AUDIT_UPDATE_DISABLED \ BUNDLER_AUDIT_ADVISORY_DB_URL \ BUNDLER_AUDIT_ADVISORY_DB_REF_NAME \ diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 891fd8c1bb5..2b08d3c63bb 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -43,7 +43,7 @@ module Gitlab end def uncached_application_settings - return fake_application_settings unless connect_to_db? + return fake_application_settings if Gitlab::Runtime.rake? && !connect_to_db? current_settings = ::ApplicationSetting.current # If there are pending migrations, it's possible there are columns that diff --git a/lib/gitlab/cycle_analytics/group_stage_summary.rb b/lib/gitlab/cycle_analytics/group_stage_summary.rb index 26eaaf7df83..09b33d01846 100644 --- a/lib/gitlab/cycle_analytics/group_stage_summary.rb +++ b/lib/gitlab/cycle_analytics/group_stage_summary.rb @@ -12,14 +12,42 @@ module Gitlab end def data - [serialize(Summary::Group::Issue.new(group: group, current_user: current_user, options: options)), - serialize(Summary::Group::Deploy.new(group: group, options: options))] + [issue_stats, + deploy_stats, + deployment_frequency_stats] end private - def serialize(summary_object) - AnalyticsSummarySerializer.new.represent(summary_object) + def issue_stats + serialize( + Summary::Group::Issue.new( + group: group, current_user: current_user, options: options) + ) + end + + def deployments_summary + @deployments_summary ||= + Summary::Group::Deploy.new(group: group, options: options) + end + + def deploy_stats + serialize deployments_summary + end + + def deployment_frequency_stats + serialize( + Summary::Group::DeploymentFrequency.new( + deployments: deployments_summary.value, + group: group, + options: options), + with_unit: true + ) + end + + def serialize(summary_object, with_unit: false) + AnalyticsSummarySerializer.new.represent( + summary_object, with_unit: with_unit) end end end diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb index 9c75d4bb455..564feb0319f 100644 --- a/lib/gitlab/cycle_analytics/stage_summary.rb +++ b/lib/gitlab/cycle_analytics/stage_summary.rb @@ -14,6 +14,7 @@ module Gitlab summary = [issue_stats] summary << commit_stats if user_has_sufficient_access? summary << deploy_stats + summary << deployment_frequency_stats end private @@ -26,16 +27,32 @@ module Gitlab serialize(Summary::Commit.new(project: @project, from: @from, to: @to)) end + def deployments_summary + @deployments_summary ||= + Summary::Deploy.new(project: @project, from: @from, to: @to) + end + def deploy_stats - serialize(Summary::Deploy.new(project: @project, from: @from, to: @to)) + serialize deployments_summary + end + + def deployment_frequency_stats + serialize( + Summary::DeploymentFrequency.new( + deployments: deployments_summary.value, + from: @from, + to: @to), + with_unit: true + ) end def user_has_sufficient_access? @project.team.member?(@current_user, Gitlab::Access::REPORTER) end - def serialize(summary_object) - AnalyticsSummarySerializer.new.represent(summary_object) + def serialize(summary_object, with_unit: false) + AnalyticsSummarySerializer.new.represent( + summary_object, with_unit: with_unit) end end end diff --git a/lib/gitlab/cycle_analytics/summary/deployment_frequency.rb b/lib/gitlab/cycle_analytics/summary/deployment_frequency.rb new file mode 100644 index 00000000000..436dc91bd6b --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/deployment_frequency.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module CycleAnalytics + module Summary + class DeploymentFrequency < Base + include SummaryHelper + + def initialize(deployments:, from:, to: nil, project: nil) + @deployments = deployments + + super(project: project, from: from, to: to) + end + + def title + _('Deployment Frequency') + end + + def value + @value ||= + frequency(@deployments, @from, @to || Time.now) + end + + def unit + _('per day') + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/group/deployment_frequency.rb b/lib/gitlab/cycle_analytics/summary/group/deployment_frequency.rb new file mode 100644 index 00000000000..9fbbbb5a1ec --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/group/deployment_frequency.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module CycleAnalytics + module Summary + module Group + class DeploymentFrequency < Group::Base + include GroupProjectsProvider + include SummaryHelper + + def initialize(deployments:, group:, options:) + @deployments = deployments + + super(group: group, options: options) + end + + def title + _('Deployment Frequency') + end + + def value + @value ||= + frequency(@deployments, options[:from], options[:to] || Time.now) + end + + def unit + _('per day') + end + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary_helper.rb b/lib/gitlab/cycle_analytics/summary_helper.rb new file mode 100644 index 00000000000..06abcd151d4 --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary_helper.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Gitlab + module CycleAnalytics + module SummaryHelper + def frequency(count, from, to) + return count if count.zero? + + freq = (count / days(from, to)).round(1) + freq.zero? ? '0' : freq + end + + def days(from, to) + [(to.end_of_day - from.beginning_of_day).fdiv(1.day), 1].max + end + end + end +end diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb index 8e699de8164..14facd6b1d4 100644 --- a/lib/gitlab/data_builder/pipeline.rb +++ b/lib/gitlab/data_builder/pipeline.rb @@ -45,7 +45,7 @@ module Gitlab target_branch: merge_request.target_branch, target_project_id: merge_request.target_project_id, state: merge_request.state, - merge_status: merge_request.merge_status, + merge_status: merge_request.public_merge_status, url: Gitlab::UrlBuilder.build(merge_request) } end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 3922f5c6683..cf5ff8ddb7b 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -184,14 +184,16 @@ module Gitlab # short period of time. The key _is_ enforced for any newly created # data. - execute <<-EOF.strip_heredoc - ALTER TABLE #{source} - ADD CONSTRAINT #{options[:name]} - FOREIGN KEY (#{options[:column]}) - REFERENCES #{target} (id) - #{on_delete_statement(options[:on_delete])} - NOT VALID; - EOF + with_lock_retries do + execute <<-EOF.strip_heredoc + ALTER TABLE #{source} + ADD CONSTRAINT #{options[:name]} + FOREIGN KEY (#{options[:column]}) + REFERENCES #{target} (id) + #{on_delete_statement(options[:on_delete])} + NOT VALID; + EOF + end end # Validate the existing constraint. This can potentially take a very diff --git a/lib/gitlab/diff/formatters/text_formatter.rb b/lib/gitlab/diff/formatters/text_formatter.rb index 5b670b1f83b..728457b3139 100644 --- a/lib/gitlab/diff/formatters/text_formatter.rb +++ b/lib/gitlab/diff/formatters/text_formatter.rb @@ -6,10 +6,12 @@ module Gitlab class TextFormatter < BaseFormatter attr_reader :old_line attr_reader :new_line + attr_reader :line_range def initialize(attrs) @old_line = attrs[:old_line] @new_line = attrs[:new_line] + @line_range = attrs[:line_range] super(attrs) end @@ -23,7 +25,7 @@ module Gitlab end def to_h - super.merge(old_line: old_line, new_line: new_line) + super.merge(old_line: old_line, new_line: new_line, line_range: line_range) end def line_age diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb index 055eae2c0fd..01ec9798fe4 100644 --- a/lib/gitlab/diff/highlight_cache.rb +++ b/lib/gitlab/diff/highlight_cache.rb @@ -94,7 +94,11 @@ module Gitlab Gitlab::Redis::Cache.with do |redis| redis.pipelined do hash.each do |diff_file_id, highlighted_diff_lines_hash| - redis.hset(key, diff_file_id, highlighted_diff_lines_hash.to_json) + redis.hset( + key, + diff_file_id, + compose_data(highlighted_diff_lines_hash.to_json) + ) end # HSETs have to have their expiration date manually updated @@ -152,12 +156,45 @@ module Gitlab end results.map! do |result| - JSON.parse(result, symbolize_names: true) unless result.nil? + JSON.parse(extract_data(result), symbolize_names: true) unless result.nil? end file_paths.zip(results).to_h end + def compose_data(json_data) + if ::Feature.enabled?(:gzip_diff_cache, default_enabled: true) + # #compress returns ASCII-8BIT, so we need to force the encoding to + # UTF-8 before caching it in redis, else we risk encoding mismatch + # errors. + # + ActiveSupport::Gzip.compress(json_data).force_encoding("UTF-8") + else + json_data + end + rescue Zlib::GzipFile::Error + json_data + end + + def extract_data(data) + # Since when we deploy this code, we'll be dealing with an already + # populated cache full of data that isn't gzipped, we want to also + # check to see if the data is gzipped before we attempt to #decompress + # it, thus we check the first 2 bytes for "\x1F\x8B" to confirm it is + # a gzipped string. While a non-gzipped string will raise a + # Zlib::GzipFile::Error, which we're rescuing, we don't want to count + # on rescue for control flow. This check can be removed in the release + # after this change is released. + # + if ::Feature.enabled?(:gzip_diff_cache, default_enabled: true) && data[0..1] == "\x1F\x8B" + ActiveSupport::Gzip.decompress(data) + else + data + end + rescue Zlib::GzipFile::Error + data + end + def cacheable?(diff_file) diffable.present? && diff_file.text? && diff_file.diffable? end diff --git a/lib/gitlab/elasticsearch/logs.rb b/lib/gitlab/elasticsearch/logs.rb deleted file mode 100644 index 3b6d1d0286a..00000000000 --- a/lib/gitlab/elasticsearch/logs.rb +++ /dev/null @@ -1,154 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Elasticsearch - class Logs - InvalidCursor = Class.new(RuntimeError) - - # How many log lines to fetch in a query - LOGS_LIMIT = 500 - - def initialize(client) - @client = client - end - - def pod_logs(namespace, pod_name: nil, container_name: nil, search: nil, start_time: nil, end_time: nil, cursor: nil) - query = { bool: { must: [] } }.tap do |q| - filter_pod_name(q, pod_name) - filter_namespace(q, namespace) - filter_container_name(q, container_name) - filter_search(q, search) - filter_times(q, start_time, end_time) - end - - body = build_body(query, cursor) - response = @client.search body: body - - format_response(response) - end - - private - - def build_body(query, cursor = nil) - body = { - query: query, - # reverse order so we can query N-most recent records - sort: [ - { "@timestamp": { order: :desc } }, - { "offset": { order: :desc } } - ], - # only return these fields in the response - _source: ["@timestamp", "message", "kubernetes.pod.name"], - # fixed limit for now, we should support paginated queries - size: ::Gitlab::Elasticsearch::Logs::LOGS_LIMIT - } - - unless cursor.nil? - body[:search_after] = decode_cursor(cursor) - end - - body - end - - def filter_pod_name(query, pod_name) - # We can filter by "all pods" with a null pod_name - return if pod_name.nil? - - query[:bool][:must] << { - match_phrase: { - "kubernetes.pod.name" => { - query: pod_name - } - } - } - end - - def filter_namespace(query, namespace) - query[:bool][:must] << { - match_phrase: { - "kubernetes.namespace" => { - query: namespace - } - } - } - end - - def filter_container_name(query, container_name) - # A pod can contain multiple containers. - # By default we return logs from every container - return if container_name.nil? - - query[:bool][:must] << { - match_phrase: { - "kubernetes.container.name" => { - query: container_name - } - } - } - end - - def filter_search(query, search) - return if search.nil? - - query[:bool][:must] << { - simple_query_string: { - query: search, - fields: [:message], - default_operator: :and - } - } - end - - def filter_times(query, start_time, end_time) - return unless start_time || end_time - - time_range = { range: { :@timestamp => {} } }.tap do |tr| - tr[:range][:@timestamp][:gte] = start_time if start_time - tr[:range][:@timestamp][:lt] = end_time if end_time - end - - query[:bool][:filter] = [time_range] - end - - def format_response(response) - results = response.fetch("hits", {}).fetch("hits", []) - last_result = results.last - results = results.map do |hit| - { - timestamp: hit["_source"]["@timestamp"], - message: hit["_source"]["message"], - pod: hit["_source"]["kubernetes"]["pod"]["name"] - } - end - - # we queried for the N-most recent records but we want them ordered oldest to newest - { - logs: results.reverse, - cursor: last_result.nil? ? nil : encode_cursor(last_result["sort"]) - } - end - - # we want to hide the implementation details of the search_after parameter from the frontend - # behind a single easily transmitted value - def encode_cursor(obj) - obj.join(',') - end - - def decode_cursor(obj) - cursor = obj.split(',').map(&:to_i) - - unless valid_cursor(cursor) - raise InvalidCursor, "invalid cursor format" - end - - cursor - end - - def valid_cursor(cursor) - cursor.instance_of?(Array) && - cursor.length == 2 && - cursor.map {|i| i.instance_of?(Integer)}.reduce(:&) - end - end - end -end diff --git a/lib/gitlab/elasticsearch/logs/lines.rb b/lib/gitlab/elasticsearch/logs/lines.rb new file mode 100644 index 00000000000..fb32a6c9fcd --- /dev/null +++ b/lib/gitlab/elasticsearch/logs/lines.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +module Gitlab + module Elasticsearch + module Logs + class Lines + InvalidCursor = Class.new(RuntimeError) + + # How many log lines to fetch in a query + LOGS_LIMIT = 500 + + def initialize(client) + @client = client + end + + def pod_logs(namespace, pod_name: nil, container_name: nil, search: nil, start_time: nil, end_time: nil, cursor: nil) + query = { bool: { must: [] } }.tap do |q| + filter_pod_name(q, pod_name) + filter_namespace(q, namespace) + filter_container_name(q, container_name) + filter_search(q, search) + filter_times(q, start_time, end_time) + end + + body = build_body(query, cursor) + response = @client.search body: body + + format_response(response) + end + + private + + def build_body(query, cursor = nil) + body = { + query: query, + # reverse order so we can query N-most recent records + sort: [ + { "@timestamp": { order: :desc } }, + { "offset": { order: :desc } } + ], + # only return these fields in the response + _source: ["@timestamp", "message", "kubernetes.pod.name"], + # fixed limit for now, we should support paginated queries + size: ::Gitlab::Elasticsearch::Logs::Lines::LOGS_LIMIT + } + + unless cursor.nil? + body[:search_after] = decode_cursor(cursor) + end + + body + end + + def filter_pod_name(query, pod_name) + # We can filter by "all pods" with a null pod_name + return if pod_name.nil? + + query[:bool][:must] << { + match_phrase: { + "kubernetes.pod.name" => { + query: pod_name + } + } + } + end + + def filter_namespace(query, namespace) + query[:bool][:must] << { + match_phrase: { + "kubernetes.namespace" => { + query: namespace + } + } + } + end + + def filter_container_name(query, container_name) + # A pod can contain multiple containers. + # By default we return logs from every container + return if container_name.nil? + + query[:bool][:must] << { + match_phrase: { + "kubernetes.container.name" => { + query: container_name + } + } + } + end + + def filter_search(query, search) + return if search.nil? + + query[:bool][:must] << { + simple_query_string: { + query: search, + fields: [:message], + default_operator: :and + } + } + end + + def filter_times(query, start_time, end_time) + return unless start_time || end_time + + time_range = { range: { :@timestamp => {} } }.tap do |tr| + tr[:range][:@timestamp][:gte] = start_time if start_time + tr[:range][:@timestamp][:lt] = end_time if end_time + end + + query[:bool][:filter] = [time_range] + end + + def format_response(response) + results = response.fetch("hits", {}).fetch("hits", []) + last_result = results.last + results = results.map do |hit| + { + timestamp: hit["_source"]["@timestamp"], + message: hit["_source"]["message"], + pod: hit["_source"]["kubernetes"]["pod"]["name"] + } + end + + # we queried for the N-most recent records but we want them ordered oldest to newest + { + logs: results.reverse, + cursor: last_result.nil? ? nil : encode_cursor(last_result["sort"]) + } + end + + # we want to hide the implementation details of the search_after parameter from the frontend + # behind a single easily transmitted value + def encode_cursor(obj) + obj.join(',') + end + + def decode_cursor(obj) + cursor = obj.split(',').map(&:to_i) + + unless valid_cursor(cursor) + raise InvalidCursor, "invalid cursor format" + end + + cursor + end + + def valid_cursor(cursor) + cursor.instance_of?(Array) && + cursor.length == 2 && + cursor.map {|i| i.instance_of?(Integer)}.reduce(:&) + end + end + end + end +end diff --git a/lib/gitlab/elasticsearch/logs/pods.rb b/lib/gitlab/elasticsearch/logs/pods.rb new file mode 100644 index 00000000000..66499ae956a --- /dev/null +++ b/lib/gitlab/elasticsearch/logs/pods.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Gitlab + module Elasticsearch + module Logs + class Pods + # How many items to fetch in a query + PODS_LIMIT = 500 + CONTAINERS_LIMIT = 500 + + def initialize(client) + @client = client + end + + def pods(namespace) + body = build_body(namespace) + response = @client.search body: body + + format_response(response) + end + + private + + def build_body(namespace) + { + aggs: { + pods: { + aggs: { + containers: { + terms: { + field: 'kubernetes.container.name', + size: ::Gitlab::Elasticsearch::Logs::Pods::CONTAINERS_LIMIT + } + } + }, + terms: { + field: 'kubernetes.pod.name', + size: ::Gitlab::Elasticsearch::Logs::Pods::PODS_LIMIT + } + } + }, + query: { + bool: { + must: { + match_phrase: { + "kubernetes.namespace": namespace + } + } + } + }, + # don't populate hits, only the aggregation is needed + size: 0 + } + end + + def format_response(response) + results = response.dig("aggregations", "pods", "buckets") || [] + results.map do |bucket| + { + name: bucket["key"], + container_names: (bucket.dig("containers", "buckets") || []).map do |cbucket| + cbucket["key"] + end + } + end + end + end + end + end +end diff --git a/lib/gitlab/error_tracking.rb b/lib/gitlab/error_tracking.rb index a6e49825fd0..b893d625f8d 100644 --- a/lib/gitlab/error_tracking.rb +++ b/lib/gitlab/error_tracking.rb @@ -186,7 +186,7 @@ module Gitlab return event unless CUSTOM_FINGERPRINTING.include?(ex.class.name) - event.fingerprint = ['{{ default }}', ex.class.name, ex.message] + event.fingerprint = [ex.class.name, ex.message] event end diff --git a/lib/gitlab/file_hook.rb b/lib/gitlab/file_hook.rb index 38c19ff506f..f23ef2921d7 100644 --- a/lib/gitlab/file_hook.rb +++ b/lib/gitlab/file_hook.rb @@ -3,16 +3,17 @@ module Gitlab module FileHook def self.any? - plugin_glob.any? { |entry| File.file?(entry) } + dir_glob.any? { |entry| File.file?(entry) } end def self.files - plugin_glob.select { |entry| File.file?(entry) } + dir_glob.select { |entry| File.file?(entry) } end - def self.plugin_glob - Dir.glob(Rails.root.join('plugins/*')) + def self.dir_glob + Dir.glob([Rails.root.join('file_hooks/*'), Rails.root.join('plugins/*')]) end + private_class_method :dir_glob def self.execute_all_async(data) args = files.map { |file| [file, data] } diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 3b9402da0dd..697c943b4ec 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -216,10 +216,6 @@ module Gitlab SafeRequestStore[:gitaly_query_time] = duration end - def self.query_time_ms - (self.query_time * 1000).round(2) - end - def self.current_transaction_labels Gitlab::Metrics::Transaction.current&.labels || {} end diff --git a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb index 045a341f2ed..ac149cadb5b 100644 --- a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb +++ b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb @@ -15,9 +15,9 @@ module Gitlab attributes = { time: datetime.utc.iso8601(3), severity: severity, - duration: time[:total], - db: time[:db], - view: time[:view] + duration_s: Gitlab::Utils.ms_to_round_sec(time[:total]), + db_duration_s: Gitlab::Utils.ms_to_round_sec(time[:db]), + view_duration_s: Gitlab::Utils.ms_to_round_sec(time[:view]) }.merge!(data) ::Lograge.formatter.call(attributes) << "\n" diff --git a/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb b/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb index 705e23adff2..fe741a5bbe8 100644 --- a/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb +++ b/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb @@ -18,9 +18,9 @@ module Gitlab return {} unless proxy_start && start_time # Time in milliseconds since gitlab-workhorse started the request - duration = (start_time.to_f * 1_000 - proxy_start.to_f / 1_000_000).round(2) + duration = start_time.to_f * 1_000 - proxy_start.to_f / 1_000_000 - { 'queue_duration': duration } + { 'queue_duration_s': Gitlab::Utils.ms_to_round_sec(duration) } end end end diff --git a/lib/gitlab/import_export/group/tree_restorer.rb b/lib/gitlab/import_export/group/legacy_tree_restorer.rb index 323e6727a9f..5d96a0f3c0a 100644 --- a/lib/gitlab/import_export/group/tree_restorer.rb +++ b/lib/gitlab/import_export/group/legacy_tree_restorer.rb @@ -3,7 +3,7 @@ module Gitlab module ImportExport module Group - class TreeRestorer + class LegacyTreeRestorer include Gitlab::Utils::StrongMemoize attr_reader :user diff --git a/lib/gitlab/instrumentation/redis.rb b/lib/gitlab/instrumentation/redis.rb index f9a6fdc05aa..6b066b800a5 100644 --- a/lib/gitlab/instrumentation/redis.rb +++ b/lib/gitlab/instrumentation/redis.rb @@ -37,10 +37,6 @@ module Gitlab ::RequestStore[REDIS_CALL_DETAILS] ||= [] end - def self.query_time_ms - (self.query_time * 1000).round(2) - end - def self.query_time ::RequestStore[REDIS_CALL_DURATION] || 0 end diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb index 5d4e6a7bdef..308c3007720 100644 --- a/lib/gitlab/instrumentation_helper.rb +++ b/lib/gitlab/instrumentation_helper.rb @@ -4,28 +4,28 @@ module Gitlab module InstrumentationHelper extend self - KEYS = %i(gitaly_calls gitaly_duration rugged_calls rugged_duration_ms redis_calls redis_duration_ms).freeze + KEYS = %i(gitaly_calls gitaly_duration_s rugged_calls rugged_duration_s redis_calls redis_duration_s).freeze def add_instrumentation_data(payload) gitaly_calls = Gitlab::GitalyClient.get_request_count if gitaly_calls > 0 payload[:gitaly_calls] = gitaly_calls - payload[:gitaly_duration] = Gitlab::GitalyClient.query_time_ms + payload[:gitaly_duration_s] = Gitlab::GitalyClient.query_time.round(2) end rugged_calls = Gitlab::RuggedInstrumentation.query_count if rugged_calls > 0 payload[:rugged_calls] = rugged_calls - payload[:rugged_duration_ms] = Gitlab::RuggedInstrumentation.query_time_ms + payload[:rugged_duration_s] = Gitlab::RuggedInstrumentation.query_time.round(2) end redis_calls = Gitlab::Instrumentation::Redis.get_request_count if redis_calls > 0 payload[:redis_calls] = redis_calls - payload[:redis_duration_ms] = Gitlab::Instrumentation::Redis.query_time_ms + payload[:redis_duration_s] = Gitlab::Instrumentation::Redis.query_time.round(2) end end @@ -47,7 +47,7 @@ module Gitlab # Its possible that if theres clock-skew between two nodes # this value may be less than zero. In that event, we record the value # as zero. - [elapsed_by_absolute_time(enqueued_at_time), 0].max + [elapsed_by_absolute_time(enqueued_at_time), 0].max.round(2) end # Calculates the time in seconds, as a float, from diff --git a/lib/gitlab/jira_import.rb b/lib/gitlab/jira_import.rb index fe4351d9029..3f56094956a 100644 --- a/lib/gitlab/jira_import.rb +++ b/lib/gitlab/jira_import.rb @@ -34,6 +34,10 @@ module Gitlab cache_class.increment(self.failed_issues_counter_cache_key(project_id)) end + def self.issue_failures(project_id) + cache_class.read(self.failed_issues_counter_cache_key(project_id)).to_i + end + def self.get_issues_next_start_at(project_id) cache_class.read(self.jira_issues_next_page_cache_key(project_id)).to_i end diff --git a/lib/gitlab/jira_import/base_importer.rb b/lib/gitlab/jira_import/base_importer.rb index 5fbdbbc08c1..5381812186d 100644 --- a/lib/gitlab/jira_import/base_importer.rb +++ b/lib/gitlab/jira_import/base_importer.rb @@ -6,10 +6,10 @@ module Gitlab attr_reader :project, :client, :formatter, :jira_project_key def initialize(project) - raise Projects::ImportService::Error, _('Jira import feature is disabled.') unless project.jira_issues_import_feature_flag_enabled? - raise Projects::ImportService::Error, _('Jira integration not configured.') unless project.jira_service&.active? + project.validate_jira_import_settings! @jira_project_key = project.latest_jira_import&.jira_project_key + raise Projects::ImportService::Error, _('Unable to find Jira project to import data from.') unless @jira_project_key @project = project diff --git a/lib/gitlab/json.rb b/lib/gitlab/json.rb new file mode 100644 index 00000000000..5ebda67e2ae --- /dev/null +++ b/lib/gitlab/json.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Json + class << self + def parse(*args) + adapter.parse(*args) + end + + def parse!(*args) + adapter.parse!(*args) + end + + def dump(*args) + adapter.dump(*args) + end + + def generate(*args) + adapter.generate(*args) + end + + def pretty_generate(*args) + adapter.pretty_generate(*args) + end + + private + + def adapter + ::JSON + end + end + end +end diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb index 2bcb428b25d..31cd21f17e0 100644 --- a/lib/gitlab/kubernetes/helm/base_command.rb +++ b/lib/gitlab/kubernetes/helm/base_command.rb @@ -25,11 +25,21 @@ module Gitlab end def service_account_resource - nil + return unless rbac? + + Gitlab::Kubernetes::ServiceAccount.new(service_account_name, namespace).generate end def cluster_role_binding_resource - nil + return unless rbac? + + subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: namespace }] + + Gitlab::Kubernetes::ClusterRoleBinding.new( + cluster_role_binding_name, + cluster_role_name, + subjects + ).generate end def file_names @@ -61,6 +71,14 @@ module Gitlab def service_account_name Gitlab::Kubernetes::Helm::SERVICE_ACCOUNT end + + def cluster_role_binding_name + Gitlab::Kubernetes::Helm::CLUSTER_ROLE_BINDING + end + + def cluster_role_name + Gitlab::Kubernetes::Helm::CLUSTER_ROLE + end end end end diff --git a/lib/gitlab/kubernetes/helm/init_command.rb b/lib/gitlab/kubernetes/helm/init_command.rb index 88ed8572ffc..058f38f2c9c 100644 --- a/lib/gitlab/kubernetes/helm/init_command.rb +++ b/lib/gitlab/kubernetes/helm/init_command.rb @@ -24,24 +24,6 @@ module Gitlab @rbac end - def service_account_resource - return unless rbac? - - Gitlab::Kubernetes::ServiceAccount.new(service_account_name, namespace).generate - end - - def cluster_role_binding_resource - return unless rbac? - - subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: namespace }] - - Gitlab::Kubernetes::ClusterRoleBinding.new( - cluster_role_binding_name, - cluster_role_name, - subjects - ).generate - end - private def init_helm_command @@ -69,14 +51,6 @@ module Gitlab ['--service-account', service_account_name] end - - def cluster_role_binding_name - Gitlab::Kubernetes::Helm::CLUSTER_ROLE_BINDING - end - - def cluster_role_name - Gitlab::Kubernetes::Helm::CLUSTER_ROLE - end end end end diff --git a/lib/gitlab/lograge/custom_options.rb b/lib/gitlab/lograge/custom_options.rb index 5dbff7d9102..145d67d7101 100644 --- a/lib/gitlab/lograge/custom_options.rb +++ b/lib/gitlab/lograge/custom_options.rb @@ -18,7 +18,7 @@ module Gitlab user_id: event.payload[:user_id], username: event.payload[:username], ua: event.payload[:ua], - queue_duration: event.payload[:queue_duration] + queue_duration_s: event.payload[:queue_duration_s] } ::Gitlab::InstrumentationHelper.add_instrumentation_data(payload) @@ -28,7 +28,7 @@ module Gitlab payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id if cpu_s = Gitlab::Metrics::System.thread_cpu_duration(::Gitlab::RequestContext.instance.start_thread_cpu_time) - payload[:cpu_s] = cpu_s + payload[:cpu_s] = cpu_s.round(2) end # https://github.com/roidrage/lograge#logging-errors--exceptions diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb index c6b898ab5aa..630788f1a8a 100644 --- a/lib/gitlab/middleware/rails_queue_duration.rb +++ b/lib/gitlab/middleware/rails_queue_duration.rb @@ -20,8 +20,10 @@ module Gitlab # Time in milliseconds since gitlab-workhorse started the request duration = Time.now.to_f * 1_000 - proxy_start.to_f / 1_000_000 trans.set(:rails_queue_duration, duration) - metric_rails_queue_duration_seconds.observe(trans.labels, duration / 1_000) - env[GITLAB_RAILS_QUEUE_DURATION_KEY] = duration.round(2) + + duration_s = Gitlab::Utils.ms_to_round_sec(duration) + metric_rails_queue_duration_seconds.observe(trans.labels, duration_s) + env[GITLAB_RAILS_QUEUE_DURATION_KEY] = duration_s end @app.call(env) diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb index beeaeb70d51..38adfc03ea7 100644 --- a/lib/gitlab/project_template.rb +++ b/lib/gitlab/project_template.rb @@ -36,33 +36,36 @@ module Gitlab name == other.name && title == other.title end - TEMPLATES_TABLE = [ - ProjectTemplate.new('rails', 'Ruby on Rails', _('Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/rails', 'illustrations/logos/rails.svg'), - ProjectTemplate.new('spring', 'Spring', _('Includes an MVC structure, mvnw and pom.xml to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/spring', 'illustrations/logos/spring.svg'), - ProjectTemplate.new('express', 'NodeJS Express', _('Includes an MVC structure to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/express', 'illustrations/logos/express.svg'), - ProjectTemplate.new('iosswift', 'iOS (Swift)', _('A ready-to-go template for use with iOS Swift apps.'), 'https://gitlab.com/gitlab-org/project-templates/iosswift', 'illustrations/logos/swift.svg'), - ProjectTemplate.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore', 'illustrations/logos/dotnet.svg'), - ProjectTemplate.new('android', 'Android', _('A ready-to-go template for use with Android apps.'), 'https://gitlab.com/gitlab-org/project-templates/android', 'illustrations/logos/android.svg'), - ProjectTemplate.new('gomicro', 'Go Micro', _('Go Micro is a framework for micro service development.'), 'https://gitlab.com/gitlab-org/project-templates/go-micro'), - ProjectTemplate.new('gatsby', 'Pages/Gatsby', _('Everything you need to create a GitLab Pages site using Gatsby.'), 'https://gitlab.com/pages/gatsby'), - ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo.'), 'https://gitlab.com/pages/hugo'), - ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll.'), 'https://gitlab.com/pages/jekyll'), - ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML.'), 'https://gitlab.com/pages/plain-html'), - ProjectTemplate.new('gitbook', 'Pages/GitBook', _('Everything you need to create a GitLab Pages site using GitBook.'), 'https://gitlab.com/pages/gitbook'), - ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo.'), 'https://gitlab.com/pages/hexo'), - ProjectTemplate.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhugo', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools.'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'), - ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg'), - ProjectTemplate.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab.'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management') - ].freeze + def self.localized_templates_table + [ + ProjectTemplate.new('rails', 'Ruby on Rails', _('Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/rails', 'illustrations/logos/rails.svg'), + ProjectTemplate.new('spring', 'Spring', _('Includes an MVC structure, mvnw and pom.xml to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/spring', 'illustrations/logos/spring.svg'), + ProjectTemplate.new('express', 'NodeJS Express', _('Includes an MVC structure to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/express', 'illustrations/logos/express.svg'), + ProjectTemplate.new('iosswift', 'iOS (Swift)', _('A ready-to-go template for use with iOS Swift apps.'), 'https://gitlab.com/gitlab-org/project-templates/iosswift', 'illustrations/logos/swift.svg'), + ProjectTemplate.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore', 'illustrations/logos/dotnet.svg'), + ProjectTemplate.new('android', 'Android', _('A ready-to-go template for use with Android apps.'), 'https://gitlab.com/gitlab-org/project-templates/android', 'illustrations/logos/android.svg'), + ProjectTemplate.new('gomicro', 'Go Micro', _('Go Micro is a framework for micro service development.'), 'https://gitlab.com/gitlab-org/project-templates/go-micro'), + ProjectTemplate.new('gatsby', 'Pages/Gatsby', _('Everything you need to create a GitLab Pages site using Gatsby.'), 'https://gitlab.com/pages/gatsby'), + ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo.'), 'https://gitlab.com/pages/hugo'), + ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll.'), 'https://gitlab.com/pages/jekyll'), + ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML.'), 'https://gitlab.com/pages/plain-html'), + ProjectTemplate.new('gitbook', 'Pages/GitBook', _('Everything you need to create a GitLab Pages site using GitBook.'), 'https://gitlab.com/pages/gitbook'), + ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo.'), 'https://gitlab.com/pages/hexo'), + ProjectTemplate.new('sse_middleman', 'Static Site Editor/Middleman', _('Middleman project with Static Site Editor support'), 'https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman'), + ProjectTemplate.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhugo', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools.'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'), + ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg'), + ProjectTemplate.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab.'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management') + ].freeze + end class << self def all - TEMPLATES_TABLE + localized_templates_table end def find(name) diff --git a/lib/gitlab/quick_actions/merge_request_actions.rb b/lib/gitlab/quick_actions/merge_request_actions.rb index e9127095a0d..7c06698ffec 100644 --- a/lib/gitlab/quick_actions/merge_request_actions.rb +++ b/lib/gitlab/quick_actions/merge_request_actions.rb @@ -8,14 +8,49 @@ module Gitlab included do # MergeRequest only quick actions definitions - desc _('Merge (when the pipeline succeeds)') - explanation _('Merges this merge request when the pipeline succeeds.') - execution_message _('Scheduled to merge this merge request when the pipeline succeeds.') + desc do + if Feature.enabled?(:merge_orchestration_service, quick_action_target.project, default_enabled: true) + if preferred_strategy = preferred_auto_merge_strategy(quick_action_target) + _("Merge automatically (%{strategy})") % { strategy: preferred_strategy.humanize } + else + _("Merge immediately") + end + else + _('Merge (when the pipeline succeeds)') + end + end + explanation do + if Feature.enabled?(:merge_orchestration_service, quick_action_target.project, default_enabled: true) + if preferred_strategy = preferred_auto_merge_strategy(quick_action_target) + _("Schedules to merge this merge request (%{strategy}).") % { strategy: preferred_strategy.humanize } + else + _('Merges this merge request immediately.') + end + else + _('Merges this merge request when the pipeline succeeds.') + end + end + execution_message do + if Feature.enabled?(:merge_orchestration_service, quick_action_target.project, default_enabled: true) + if preferred_strategy = preferred_auto_merge_strategy(quick_action_target) + _("Scheduled to merge this merge request (%{strategy}).") % { strategy: preferred_strategy.humanize } + else + _('Merged this merge request.') + end + else + _('Scheduled to merge this merge request when the pipeline succeeds.') + end + end types MergeRequest condition do - last_diff_sha = params && params[:merge_request_diff_head_sha] - quick_action_target.persisted? && - quick_action_target.mergeable_with_quick_action?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha) + if Feature.enabled?(:merge_orchestration_service, quick_action_target.project, default_enabled: true) + quick_action_target.persisted? && + merge_orchestration_service.can_merge?(quick_action_target) + else + last_diff_sha = params && params[:merge_request_diff_head_sha] + quick_action_target.persisted? && + quick_action_target.mergeable_with_quick_action?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha) + end end command :merge do @updates[:merge] = params[:merge_request_diff_head_sha] @@ -70,6 +105,14 @@ module Gitlab @updates[:target_branch] = branch_name if project.repository.branch_exists?(branch_name) end end + + def merge_orchestration_service + @merge_orchestration_service ||= MergeRequests::MergeOrchestrationService.new(project, current_user) + end + + def preferred_auto_merge_strategy(merge_request) + merge_orchestration_service.preferred_auto_merge_strategy(merge_request) + end end end end diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index d74e64116ca..0473fa89a0d 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -194,3 +194,5 @@ module Gitlab # rubocop: enable CodeReuse/ActiveRecord end end + +Gitlab::SearchResults.prepend_if_ee('EE::Gitlab::SearchResults') diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb index af9072ea201..ea60190353e 100644 --- a/lib/gitlab/sidekiq_logging/structured_logger.rb +++ b/lib/gitlab/sidekiq_logging/structured_logger.rb @@ -50,27 +50,27 @@ module Gitlab message = base_message(payload) if job_exception - payload['message'] = "#{message}: fail: #{payload['duration']} sec" + payload['message'] = "#{message}: fail: #{payload['duration_s']} sec" payload['job_status'] = 'fail' payload['error_message'] = job_exception.message payload['error_class'] = job_exception.class.name else - payload['message'] = "#{message}: done: #{payload['duration']} sec" + payload['message'] = "#{message}: done: #{payload['duration_s']} sec" payload['job_status'] = 'done' end - payload['db_duration'] = ActiveRecord::LogSubscriber.runtime - payload['db_duration_s'] = payload['db_duration'] / 1000 + db_duration = ActiveRecord::LogSubscriber.runtime + payload['db_duration_s'] = Gitlab::Utils.ms_to_round_sec(db_duration) payload end def add_time_keys!(time, payload) - payload['duration'] = time[:duration].round(6) + payload['duration_s'] = time[:duration].round(2) # ignore `cpu_s` if the platform does not support Process::CLOCK_THREAD_CPUTIME_ID (time[:cputime] == 0) # supported OS version can be found at: https://www.rubydoc.info/stdlib/core/2.1.6/Process:clock_gettime - payload['cpu_s'] = time[:cputime].round(6) if time[:cputime] > 0 + payload['cpu_s'] = time[:cputime].round(2) if time[:cputime] > 0 payload['completed_at'] = Time.now.utc.to_f end diff --git a/lib/gitlab/sidekiq_middleware/server_metrics.rb b/lib/gitlab/sidekiq_middleware/server_metrics.rb index 60618787b24..61ed2fe1a06 100644 --- a/lib/gitlab/sidekiq_middleware/server_metrics.rb +++ b/lib/gitlab/sidekiq_middleware/server_metrics.rb @@ -71,7 +71,7 @@ module Gitlab end def get_gitaly_time(job) - job.fetch(:gitaly_duration, 0) / 1000.0 + job.fetch(:gitaly_duration_s, 0) end end end diff --git a/lib/gitlab/signed_commit.rb b/lib/gitlab/signed_commit.rb index 809e0a3f034..7a154978938 100644 --- a/lib/gitlab/signed_commit.rb +++ b/lib/gitlab/signed_commit.rb @@ -4,6 +4,8 @@ module Gitlab class SignedCommit include Gitlab::Utils::StrongMemoize + delegate :id, to: :@commit + def initialize(commit) @commit = commit diff --git a/lib/gitlab/slash_commands/presenters/base.rb b/lib/gitlab/slash_commands/presenters/base.rb index 08de9df14f8..b60f0b78fef 100644 --- a/lib/gitlab/slash_commands/presenters/base.rb +++ b/lib/gitlab/slash_commands/presenters/base.rb @@ -91,7 +91,7 @@ module Gitlab title: "#{issue.title} ยท #{issue.to_reference}", title_link: resource_url, author_name: author.name, - author_icon: author.avatar_url, + author_icon: author.avatar_url(only_path: false), fallback: fallback_message, pretext: custom_pretext, text: text, diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb index ad6b213bb50..2e8a3ca4242 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -122,6 +122,10 @@ module Gitlab bytes.to_f / Numeric::MEGABYTE end + def ms_to_round_sec(ms) + (ms.to_f / 1000).round(2) + end + # Used in EE # Accepts either an Array or a String and returns an array def ensure_array_from_string(string_or_array) diff --git a/lib/system_check/app/redis_version_check.rb b/lib/system_check/app/redis_version_check.rb index da695cc9272..aca2972f287 100644 --- a/lib/system_check/app/redis_version_check.rb +++ b/lib/system_check/app/redis_version_check.rb @@ -1,24 +1,43 @@ # frozen_string_literal: true +require 'redis' + module SystemCheck module App class RedisVersionCheck < SystemCheck::BaseCheck - MIN_REDIS_VERSION = '2.8.0' - set_name "Redis version >= #{MIN_REDIS_VERSION}?" + MIN_REDIS_VERSION = '3.2.0' + RECOMMENDED_REDIS_VERSION = '4.0.0' + set_name "Redis version >= #{RECOMMENDED_REDIS_VERSION}?" + + @custom_error_message = '' def check? - redis_version = run_command(%w(redis-cli --version)) - redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/) + redis_version = Gitlab::Redis::Queues.with do |redis| + redis.info['redis_version'] + end + + status = true + + if !redis_version + @custom_error_message = "Could not retrieve the Redis version. Please check if your settings are correct" + status = false + elsif Gem::Version.new(redis_version) < Gem::Version.new(MIN_REDIS_VERSION) + @custom_error_message = "Your Redis version #{redis_version} is not supported anymore. Update your Redis server to a version >= #{RECOMMENDED_REDIS_VERSION}" + status = false + elsif Gem::Version.new(redis_version) < Gem::Version.new(RECOMMENDED_REDIS_VERSION) + @custom_error_message = "Support for your Redis version #{redis_version} has been deprecated and will be removed soon. Update your Redis server to a version >= #{RECOMMENDED_REDIS_VERSION}" + status = false + end - redis_version && (Gem::Version.new(redis_version[1]) > Gem::Version.new(MIN_REDIS_VERSION)) + status end def show_error try_fixing_it( - "Update your redis server to a version >= #{MIN_REDIS_VERSION}" + @custom_error_message ) for_more_information( - 'gitlab-public-wiki/wiki/Trouble-Shooting-Guide in section sidekiq' + 'doc/administration/high_availability/redis.md#provide-your-own-redis-instance-core-only' ) fix_and_rerun end diff --git a/lib/tasks/file_hooks.rake b/lib/tasks/file_hooks.rake index 20a726de65b..66d382db612 100644 --- a/lib/tasks/file_hooks.rake +++ b/lib/tasks/file_hooks.rake @@ -1,7 +1,7 @@ namespace :file_hooks do - desc 'Validate existing plugins' + desc 'Validate existing file hooks' task validate: :environment do - puts 'Validating file hooks from /plugins directory' + puts 'Validating file hooks from /file_hooks and /plugins directories' Gitlab::FileHook.files.each do |file| success, message = Gitlab::FileHook.execute(file, Gitlab::DataBuilder::Push::SAMPLE_DATA) |